Skip to content

Health Probes: For llm_call endpoints - #1021

Open
Prajna1999 wants to merge 14 commits into
mainfrom
feat/api-health-probes
Open

Health Probes: For llm_call endpoints#1021
Prajna1999 wants to merge 14 commits into
mainfrom
feat/api-health-probes

Conversation

@Prajna1999

@Prajna1999 Prajna1999 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Issue #987

Summary

A cron job that wakes up every 5 mins and make API calls with a small payload to every major models. If any error occurs i.e inference for the model is down it shows up in Sentry Issues. The call traces are logged in sentry too.

Added automated health probes for configured text, speech-to-text, and text-to-speech providers.

  • Health probes now run on a scheduled basis and report success, failures, latency, and skipped status.

Models count 13

Probe(provider="openai", model="gpt-4o-mini", modality="text"),
    Probe(provider="google", model="gemini-2.5-flash", modality="text"),
    Probe(provider="google", model="gemini-2.5-pro", modality="text"),
    # TTS
    Probe(provider="google", model="gemini-2.5-flash-preview-tts", modality="tts"),
    Probe(provider="google", model="gemini-3.1-flash-preview-tts", modality="tts"),
    Probe(provider="google", model="gemini-2.5-pro-preview-tts", modality="tts"),
    Probe(provider="sarvamai", model="bulbul:v3", modality="tts"),
    Probe(provider="elevenlabs", model="eleven_v3", modality="tts"),
    # STT
    Probe(provider="google", model="gemini-2.5-pro", modality="stt"),
    Probe(provider="google", model="gemini-2.5-flash", modality="stt"),
    Probe(provider="google", model="gemini-3.1-pro-preview", modality="stt"),
    Probe(provider="sarvamai", model="saaras:v3", modality="stt"),
    Probe(provider="elevenlabs", model="scribe_v2", modality="stt"),

The job happens inside Celery task.
env addition

HEALTH_PROBE_ORG_ID= 1
HEALTH_PROBE_PROJECT_ID=2
HEALTH_PROBE_INTERVAL_MINUTES=1

The superuser org_id and project_id against whose creds are to be used to make the provider calls. This to be set during deployment.

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Summary by CodeRabbit

  • New Features

    • Added automated health probes that rotate across text, speech, and audio checks.
    • Health probes now run through the standard job pipeline, with results tracked and reported through monitoring.
    • Cron responses now include probe and job status details.
    • Health probe scheduling frequency increased to every three minutes.
  • Documentation

    • Added health probe design documentation and execution flow diagrams.
    • Updated platform documentation with health probe behavior.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Prajna1999, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 73db5924-d42b-4d45-ae65-c34eea05c035

📥 Commits

Reviewing files that changed from the base of the PR and between b8c6aa0 and 45d6b0d.

📒 Files selected for processing (2)
  • backend/app/api/routes/cron.py
  • backend/app/core/config.py
📝 Walkthrough

Walkthrough

Changes

The health probe system now schedules one rotating probe per cron tick through Redis and the existing job pipeline. The protected cron route runs the tick directly, reports the previous job outcome to Sentry, and returns tick metadata. Probe configuration, telemetry propagation, tests, and documentation were updated.

Health probe cron flow

Layer / File(s) Summary
Probe registry and tick orchestration
backend/app/core/config.py, backend/app/services/health_probes.py, backend/app/tests/services/*
Defines modality-specific probes, builds LLM call requests, rotates probe indexes in Redis, checks prior job status, emits Sentry check-ins, and enqueues one probe per tick with coverage for rotation and failure cases.
Cron route and job-pipeline transition
backend/app/api/routes/cron.py, backend/app/celery/tasks/job_execution.py, scripts/python/invoke-cron.py, backend/app/core/telemetry.py
Routes cron execution to run_health_probe_tick(), removes the obsolete Celery task, registers the endpoint with the invoker, and configures Sentry trace propagation.
Cron contract and authorization tests
backend/app/tests/api/routes/test_cron_health_probes.py
Verifies direct tick-result responses, authorization behavior, and continued OpenAPI exclusion.
Health probe specification and documentation
docs/wiki/*, features/health-probes/*
Documents the Redis state, job-table usage, endpoint contract, probe pipeline, and provider-specific parameters.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant CronRoute
  participant HealthProbeService
  participant Redis
  participant JobDatabase
  participant StartJob
  participant Sentry
  Scheduler->>CronRoute: GET /api/v1/cron/health-probes
  CronRoute->>HealthProbeService: run_health_probe_tick()
  HealthProbeService->>Redis: Read previous job and claim next index
  HealthProbeService->>JobDatabase: Read previous Job.status
  HealthProbeService->>Sentry: Report previous status
  HealthProbeService->>StartJob: Enqueue selected probe
  HealthProbeService->>Redis: Store new job id
  CronRoute-->>Scheduler: Return enqueued tick metadata
Loading

Possibly related PRs

Suggested reviewers: ayush8923

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the main change: adding health probes for the llm_call pipeline and endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-health-probes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot changed the title resolve merge conflict chore(api): Resolve merge conflicts Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

OpenAPI changes   ⚪ No API surface changes

Note

This PR does not modify the API contract.

main5ace1fc2 · generated by oasdiff

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.67399% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
backend/app/services/health_probes.py 86.86% 13 Missing ⚠️
backend/app/api/routes/cron.py 69.23% 4 Missing ⚠️
backend/app/core/telemetry.py 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Prajna1999 Prajna1999 changed the title chore(api): Resolve merge conflicts Health Probes: For llm_call endpoints Jul 10, 2026
@Prajna1999 Prajna1999 self-assigned this Jul 10, 2026
@Prajna1999 Prajna1999 linked an issue Jul 10, 2026 that may be closed by this pull request

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
backend/app/services/health_probes.py (1)

211-220: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a uniqueness assertion to the __main__ self-check.

The __main__ block validates modalities and config building but doesn't check for duplicate probes. Adding assert len(_PROBES) == len(set(_PROBES)) would have caught the duplicate at lines 40-41.

♻️ Proposed addition
 if __name__ == "__main__":
     assert _PROBES, "probe list must not be empty"
+    assert len(_PROBES) == len(set(_PROBES)), "duplicate probe entries detected"
     modalities = {p.modality for p in _PROBES}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/health_probes.py` around lines 211 - 220, Add a
duplicate-probe validation to the __main__ self-check in health_probes.py: after
asserting _PROBES is nonempty, assert len(_PROBES) == len(set(_PROBES)) before
validating modalities and config construction.
backend/app/tests/services/test_health_probes.py (1)

131-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the obscure generator-throw lambda with a named function for consistency.

lambda **_k: (_ for _ in ()).throw(RuntimeError("bad")) is unreadable. The ValueError test above (line 117) uses a clear named function _boom — follow the same pattern here.

♻️ Proposed fix
-        monkeypatch.setattr(
-            health_probes,
-            "get_llm_provider",
-            lambda **_k: (_ for _ in ()).throw(RuntimeError("bad")),
-        )
+        def _boom(**_kwargs):
+            raise RuntimeError("bad")
+
+        monkeypatch.setattr(health_probes, "get_llm_provider", _boom)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/tests/services/test_health_probes.py` around lines 131 - 138,
Replace the generator-throw lambda in
test_get_llm_provider_runtime_error_marks_client_init_failed with a local named
function, such as _boom, that accepts arbitrary keyword arguments and raises
RuntimeError("bad"), then monkeypatch get_llm_provider with that function,
matching the pattern used by the ValueError test.
backend/app/celery/tasks/job_execution.py (1)

411-423: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using dict[str, Any] instead of bare dict for return types.

The upstream run_probes returns dict[str, Any]. Using -> dict[str, Any] on both run_health_probes and the nested _do closure would match that contract and improve type precision.

♻️ Proposed type hint refinement
-def run_health_probes(self, trace_id: str = DEFAULT_TRACE_ID) -> dict:
+def run_health_probes(self, trace_id: str = DEFAULT_TRACE_ID) -> dict[str, Any]:
     from sqlmodel import Session
 
     from app.core.db import engine
     from app.services.health_probes import run_probes
 
     _set_trace(trace_id)
 
-    def _do() -> dict:
+    def _do() -> dict[str, Any]:
         with Session(engine) as session:
             return run_probes(session=session)
 
     return _run_with_otel_parent(self, _do)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/celery/tasks/job_execution.py` around lines 411 - 423, Update the
return annotations of both `run_health_probes` and its nested `_do` closure from
bare `dict` to `dict[str, Any]`, importing `Any` as needed, so they match the
`run_probes` return contract.
backend/app/api/routes/cron.py (2)

35-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared monitor config values into constants.

checkin_margin, failure_issue_threshold, and recovery_threshold are duplicated between EVALUATION_CRON_MONITOR_CONFIG and the new HEALTH_PROBES_CRON_MONITOR_CONFIG with identical values. As per coding guidelines, repeated literals should be extracted into constants.

♻️ Proposed refactor
+CRON_CHECKIN_MARGIN = 2
+CRON_FAILURE_ISSUE_THRESHOLD = 2
+CRON_RECOVERY_THRESHOLD = 1
+
 EVALUATION_CRON_MONITOR_CONFIG: MonitorConfig = {
     "schedule": {
         "type": "interval",
         "value": settings.CRON_INTERVAL_MINUTES,
         "unit": "minute",
     },
     "timezone": "UTC",
-    "checkin_margin": 2,
+    "checkin_margin": CRON_CHECKIN_MARGIN,
     "max_runtime": 2 * settings.CRON_INTERVAL_MINUTES,
-    "failure_issue_threshold": 2,
-    "recovery_threshold": 1,
+    "failure_issue_threshold": CRON_FAILURE_ISSUE_THRESHOLD,
+    "recovery_threshold": CRON_RECOVERY_THRESHOLD,
 }

 HEALTH_PROBES_CRON_MONITOR_CONFIG: MonitorConfig = {
     "schedule": {
         "type": "interval",
         "value": settings.HEALTH_PROBE_INTERVAL_MINUTES,
         "unit": "minute",
     },
     "timezone": "UTC",
-    "checkin_margin": 2,
+    "checkin_margin": CRON_CHECKIN_MARGIN,
     "max_runtime": 2 * settings.HEALTH_PROBE_INTERVAL_MINUTES,
-    "failure_issue_threshold": 2,
-    "recovery_threshold": 1,
+    "failure_issue_threshold": CRON_FAILURE_ISSUE_THRESHOLD,
+    "recovery_threshold": CRON_RECOVERY_THRESHOLD,
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/routes/cron.py` around lines 35 - 47, Extract the duplicated
monitor settings into shared constants and use them in both
EVALUATION_CRON_MONITOR_CONFIG and HEALTH_PROBES_CRON_MONITOR_CONFIG. Replace
the repeated checkin_margin, failure_issue_threshold, and recovery_threshold
literals with the new constants while preserving their current values.

Source: Coding guidelines


148-151: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add error handling for consistency with other cron endpoints.

evaluation_cron_job and pending_jobs_cron_job both wrap their logic in try/except, logging the error and capturing it in Sentry before re-raising. health_probes_cron_job has no such guard — if run_health_probes.delay() fails (e.g., broker unavailable), the exception propagates without structured logging or explicit Sentry capture.

🛡️ Proposed error handling
 def health_probes_cron_job() -> dict:
     logger.info("[health_probes_cron_job] Cron job invoked — enqueueing task")
-    async_result = run_health_probes.delay()
-    return {"enqueued": True, "task_id": async_result.id}
+    try:
+        async_result = run_health_probes.delay()
+        return {"enqueued": True, "task_id": async_result.id}
+    except Exception as e:
+        logger.error(
+            f"[health_probes_cron_job] Error enqueueing health probes: {e}",
+            exc_info=True,
+        )
+        sentry_sdk.capture_exception(e)
+        raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/routes/cron.py` around lines 148 - 151, Wrap the logic in
health_probes_cron_job with a try/except matching evaluation_cron_job and
pending_jobs_cron_job: log the exception with context, capture it in Sentry,
then re-raise it. Keep the existing enqueue and response behavior inside the try
block.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/services/health_probes.py`:
- Around line 40-41: Remove the duplicate Probe entry for provider "google",
model "gemini-2.5-flash", and modality "text" from the probe configuration, or
replace it with the intended distinct model while preserving the existing Probe
structure.
- Around line 134-147: Wrap the _build_config_and_input(probe) call in a
dedicated try-except that catches validation and decoding errors, records an
appropriate per-probe error in result, and returns it without propagating to
ThreadPoolExecutor.map. Preserve the existing stt_audio_not_configured handling
for a None result, while keeping provider.execute’s existing exception handling
separate.

In `@backend/app/tests/celery/test_run_health_probes.py`:
- Line 12: Add the missing None return annotations to
_NonClosingSession.__init__ and both test functions, preserving their existing
parameters and behavior.

---

Nitpick comments:
In `@backend/app/api/routes/cron.py`:
- Around line 35-47: Extract the duplicated monitor settings into shared
constants and use them in both EVALUATION_CRON_MONITOR_CONFIG and
HEALTH_PROBES_CRON_MONITOR_CONFIG. Replace the repeated checkin_margin,
failure_issue_threshold, and recovery_threshold literals with the new constants
while preserving their current values.
- Around line 148-151: Wrap the logic in health_probes_cron_job with a
try/except matching evaluation_cron_job and pending_jobs_cron_job: log the
exception with context, capture it in Sentry, then re-raise it. Keep the
existing enqueue and response behavior inside the try block.

In `@backend/app/celery/tasks/job_execution.py`:
- Around line 411-423: Update the return annotations of both `run_health_probes`
and its nested `_do` closure from bare `dict` to `dict[str, Any]`, importing
`Any` as needed, so they match the `run_probes` return contract.

In `@backend/app/services/health_probes.py`:
- Around line 211-220: Add a duplicate-probe validation to the __main__
self-check in health_probes.py: after asserting _PROBES is nonempty, assert
len(_PROBES) == len(set(_PROBES)) before validating modalities and config
construction.

In `@backend/app/tests/services/test_health_probes.py`:
- Around line 131-138: Replace the generator-throw lambda in
test_get_llm_provider_runtime_error_marks_client_init_failed with a local named
function, such as _boom, that accepts arbitrary keyword arguments and raises
RuntimeError("bad"), then monkeypatch get_llm_provider with that function,
matching the pattern used by the ValueError test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4862ba33-39e8-4bae-b167-cbf6f204e8c9

📥 Commits

Reviewing files that changed from the base of the PR and between 102b61c and cdc3ac6.

📒 Files selected for processing (9)
  • backend/app/api/routes/cron.py
  • backend/app/celery/tasks/job_execution.py
  • backend/app/core/config.py
  • backend/app/core/telemetry.py
  • backend/app/services/health_probes.py
  • backend/app/tests/api/routes/test_cron_health_probes.py
  • backend/app/tests/celery/test_run_health_probes.py
  • backend/app/tests/services/test_health_probes.py
  • scripts/python/invoke-cron.py

Comment thread backend/app/services/health_probes.py Outdated
Comment thread backend/app/services/health_probes.py Outdated
Comment thread backend/app/tests/celery/test_run_health_probes.py Outdated
Prajna1999 and others added 3 commits July 10, 2026 09:57
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@Prajna1999 Prajna1999 added enhancement New feature or request and removed chore labels Jul 10, 2026
@Prajna1999 Prajna1999 moved this to In Review in Kaapi-dev Jul 10, 2026
@Prajna1999 Prajna1999 removed this from Kaapi-dev Jul 10, 2026

@vprashrex vprashrex left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have approved the PR so it doesn't block the merge, but I added comments that should be addressed before this goes in:
Database sessions should never be opened at the task level. They should be short-lived and opened only within the child functions that perform the database operations.

Comment thread backend/app/api/routes/cron.py Outdated
HEALTH_PROBES_CRON_MONITOR_CONFIG: MonitorConfig = {
"schedule": {
"type": "interval",
# not required eventbridge is aleady 5 mins. So not required.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

_set_trace(trace_id)

def _do() -> dict:
with Session(engine) as session:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening a DB session at the task level isn't a good approach because it keeps the connection open for the entire task duration. Sessions should be short-lived. Instead, open and close the session within the child functions where the database operations actually happen.

Comment thread backend/app/services/health_probes.py Outdated
_PROBE_MAX_TOKENS = 1
_PROBE_WORKERS = 4

# "Hello" ~1sec

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of inlining the Base64 payload in the source file, consider storing it in a separate file and reading it at runtime. This keeps the source code clean, improves maintainability, and avoids checking large static blobs into the code.

like this u can do

with open("tests/assets/hello.wav", "rb") as f:
    audio_b64 = base64.b64encode(f.read()).decode()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


_PROBES: list[Probe] = [
# Text
Probe(provider="openai", model="gpt-4o-mini", modality="text"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use anthropic provider as well to test

@Prajna1999
Prajna1999 requested a review from kartpop July 20, 2026 04:38

@kartpop kartpop left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found at least two major issues - please fix

Comment thread backend/app/services/health_probes.py Outdated
_PROBE_MAX_TOKENS = 1
_PROBE_WORKERS = 4

# "Hello" ~1sec

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment thread backend/app/services/health_probes.py Outdated
Comment on lines +75 to +116
def _build_config_and_input(
probe: Probe,
) -> tuple[KaapiCompletionConfig, str | AudioRef] | None:
if probe.modality == "text":
cfg = KaapiCompletionConfig.model_validate(
{
"provider": probe.provider,
"type": "text",
"params": {
"model": probe.model,
"temperature": 0.0,
"max_output_tokens": _PROBE_MAX_TOKENS,
},
}
)
return cfg, _PROBE_INPUT

if probe.modality == "tts":
cfg = KaapiCompletionConfig.model_validate(
{
"provider": probe.provider,
"type": "tts",
"params": {"model": probe.model},
}
)
return cfg, _PROBE_INPUT

# stt
if not _STT_AUDIO_B64:
return None
cfg = KaapiCompletionConfig.model_validate(
{
"provider": probe.provider,
"type": "stt",
"params": {"model": probe.model},
}
)
audio = AudioRef(
bytes_=base64.b64decode(_STT_AUDIO_B64),
mime_type=_STT_AUDIO_MIME,
)
return cfg, audio

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this will work!!!

shouldn't transform_kaapi_config_to_native be called like it gets called in the llm_call flow?

passing KappiCompletionConfig will probably send malformed requests - did we test this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this was working manually. I will retest and confirm

Comment thread backend/app/api/routes/cron.py Outdated
Comment on lines +139 to +151
@router.get(
"/cron/health-probes",
include_in_schema=False,
dependencies=[Depends(require_permission(Permission.SUPERUSER))],
)
@sentry_sdk.monitor(
monitor_slug="health-probes-cron-job",
monitor_config=HEALTH_PROBES_CRON_MONITOR_CONFIG,
)
def health_probes_cron_job() -> dict:
logger.info("[health_probes_cron_job] Cron job invoked — enqueueing task")
async_result = run_health_probes.delay()
return {"enqueued": True, "task_id": async_result.id}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this just checks whether the task is enqueued right? is it doing anything useful? i think this will mark SUCCESS irrespective of what happens in the celery task

@vprashrex can you confirm?

there should be a sentry monitor on the task also, because the real work is happening inside the celery task.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sentry_sdk.monitor is currently on the route, so it reports OK as soon as the Celery task is queued, not when it actually finishes. Probe failures only appear via logger.error, which isn't real monitoring and misses warning/skipped cases. Need to move the check-in into run_health_probes using capture_checkin, so the monitor reports OK only when all probes succeed and ERROR otherwise.

@Ayush8923

Ayush8923 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

I have updated the PR label from ready-for-review to reviewed since @kartpop and @vprashrex have both reviewed it. please address the review comments. Once they are resolved, remove the reviewed label and add ready-for-review again.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
backend/app/tests/services/test_health_probes.py (1)

21-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add missing -> None return type hints on __init__ methods.

_NonClosingSession.__init__ (line 25) and _FakeProvider.__init__ (lines 41-46) lack return type annotations.

As per coding guidelines, "provide narrow type hints for every function parameter and return value."

✏️ Proposed fix
-    def __init__(self, session: Session):
+    def __init__(self, session: Session) -> None:
         self._session = session
     def __init__(
         self,
         response: Any = None,
         error: str | None = None,
         raise_exc: Exception | None = None,
-    ):
+    ) -> None:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/tests/services/test_health_probes.py` around lines 21 - 50, Add
the explicit -> None return annotation to the __init__ methods of
_NonClosingSession and _FakeProvider, preserving their existing parameters and
initialization logic.

Source: Coding guidelines

backend/app/services/health_probes.py (1)

160-241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Previous critical concern resolved — config build now inside try-except in _prepare_probes.

_build_config_and_input is now called inside a dedicated try-except (lines 227-236) rather than unguarded inside _run_probe, so a ValidationError/decode failure on one probe no longer aborts the whole run.

Separately, several probe outcomes use hard-coded string literals ("client_init_failed", "stt_audio_not_configured", "no_response", "not_prepared") that are also asserted against by string equality in the test suite. Consider a small ProbeErrorEnum (or module-level constants) shared between this module and its tests to avoid silent drift between producer and consumer strings.

As per coding guidelines, "Do not use magic values; extract repeated literals into constants, enums, or settings" and "Use an Enum suffix for enum class names."

♻️ Proposed refactor
+class ProbeErrorEnum(str, Enum):
+    CLIENT_INIT_FAILED = "client_init_failed"
+    STT_AUDIO_NOT_CONFIGURED = "stt_audio_not_configured"
+    NO_RESPONSE = "no_response"
+    NOT_PREPARED = "not_prepared"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/health_probes.py` around lines 160 - 241, Extract the
repeated probe error literals used by _run_probe and
_prepare_probes—"client_init_failed", "stt_audio_not_configured", "no_response",
and "not_prepared"—into shared module-level constants or a ProbeErrorEnum, using
the required Enum suffix if defining an enum. Replace the producer-side literals
with those shared symbols and update tests to assert against the same symbols
while preserving the existing serialized string values.

Source: Coding guidelines

backend/app/celery/tasks/job_execution.py (1)

248-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing type hints on run_prompt_improvement.

Return type and **kwargs are untyped, unlike the sibling run_health_probes task.

As per coding guidelines, "provide narrow type hints for every function parameter and return value; do not use -> Any as a substitute for a specific annotation."

✏️ Proposed fix
-def run_prompt_improvement(self, project_id: int, job_id: str, trace_id: str, **kwargs):
+def run_prompt_improvement(
+    self, project_id: int, job_id: str, trace_id: str, **kwargs: Any
+) -> dict:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/celery/tasks/job_execution.py` around lines 248 - 266, Update
run_prompt_improvement with narrow type annotations for its **kwargs parameter
and return value, matching the typing approach used by the sibling
run_health_probes task; avoid using Any and preserve the existing task execution
behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/app/celery/tasks/job_execution.py`:
- Around line 248-266: Update run_prompt_improvement with narrow type
annotations for its **kwargs parameter and return value, matching the typing
approach used by the sibling run_health_probes task; avoid using Any and
preserve the existing task execution behavior.

In `@backend/app/services/health_probes.py`:
- Around line 160-241: Extract the repeated probe error literals used by
_run_probe and _prepare_probes—"client_init_failed", "stt_audio_not_configured",
"no_response", and "not_prepared"—into shared module-level constants or a
ProbeErrorEnum, using the required Enum suffix if defining an enum. Replace the
producer-side literals with those shared symbols and update tests to assert
against the same symbols while preserving the existing serialized string values.

In `@backend/app/tests/services/test_health_probes.py`:
- Around line 21-50: Add the explicit -> None return annotation to the __init__
methods of _NonClosingSession and _FakeProvider, preserving their existing
parameters and initialization logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 468b52fd-540d-451e-bdb0-74d9f25c4b25

📥 Commits

Reviewing files that changed from the base of the PR and between cdc3ac6 and 1ebef3d.

⛔ Files ignored due to path filters (1)
  • backend/app/assets/health_probe_hello.ogg is excluded by !**/*.ogg
📒 Files selected for processing (7)
  • backend/app/api/routes/cron.py
  • backend/app/celery/tasks/job_execution.py
  • backend/app/core/config.py
  • backend/app/services/health_probes.py
  • backend/app/tests/api/routes/test_cron_health_probes.py
  • backend/app/tests/celery/test_run_health_probes.py
  • backend/app/tests/services/test_health_probes.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/app/tests/api/routes/test_cron_health_probes.py
  • backend/app/core/config.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/routes/cron.py`:
- Around line 131-140: Wrap the work in health_probes_cron_job, including
run_health_probe_tick and completion logging, in the same try/except pattern
used by evaluation_cron_job: log the failure, call sentry_sdk.capture_exception
with the caught exception, then re-raise it to preserve existing propagation
behavior.

In `@backend/app/services/health_probes.py`:
- Around line 152-175: Update _check_previous_probe to handle malformed Redis
job IDs without propagating an exception: validate or parse last_job_id before
calling JobCrud.get, catch UUID parsing failures, log a warning consistent with
the existing messages, and return None so the health-probe tick can continue and
later overwrite the key. Also guard the JobCrud.get database call against its
expected failure path, logging the error and returning None while preserving the
existing behavior for missing jobs and valid statuses.

In `@features/health-probes/SRD.md`:
- Line 45: Replace the PLACE IMAGE HERE marker in SRD.md with the supplied
assets/flow-a.mmd diagram, either by embedding the Mermaid source or linking a
generated image, so the cron/probe flow renders visibly instead of showing
placeholder text.
- Line 31: Update the Redis rotation claims in SRD.md, including FR-9 and the
references to health_probe:index, to state that collision-free slot selection is
guaranteed only when atomic Redis INCR succeeds; document that RedisError
fallback may cause overlapping ticks to select the same probe.
- Line 87: Update the response contract around previous_job_status in SRD.md to
document every path that yields null, including Redis access failure, a missing
referenced job, absent probe configuration, and missing or expired
health_probe:last_job_id. Alternatively, expose distinct skip or error reasons
for these cases instead of representing them all as null.
- Around line 72-74: Update features/health-probes/SRD.md lines 72-74 to
document that missing organization or project settings cause the health-probes
route to return enqueued: false, or modify the route to fail visibly instead.
Update features/health-probes/assets/flow-a.mmd line 24 to show an alternate
branch for the non-enqueued response.
- Line 25: Align the documented missing Redis-state behavior with the
implementation: in features/health-probes/SRD.md at lines 25 and 61, either add
explicit Sentry instrumentation and logging for missing rotation-state and
health_probe:index values, or remove/revise those Sentry-visible and
explicit-logging requirements; in features/health-probes/assets/flow-a.mmd lines
16-18, depict the logger-only skipped-state branch unless Sentry check-in
behavior is implemented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dce2060a-7225-4ff9-bc30-c17e0a3527b9

📥 Commits

Reviewing files that changed from the base of the PR and between 1ebef3d and b8c6aa0.

⛔ Files ignored due to path filters (1)
  • features/health-probes/assets/flow-a.png is excluded by !**/*.png
📒 Files selected for processing (10)
  • backend/app/api/routes/cron.py
  • backend/app/celery/tasks/job_execution.py
  • backend/app/core/config.py
  • backend/app/services/health_probes.py
  • backend/app/tests/api/routes/test_cron_health_probes.py
  • backend/app/tests/services/test_health_probes.py
  • docs/wiki/INDEX.md
  • docs/wiki/modules/platform.md
  • features/health-probes/SRD.md
  • features/health-probes/assets/flow-a.mmd
💤 Files with no reviewable changes (1)
  • backend/app/celery/tasks/job_execution.py

Comment thread backend/app/api/routes/cron.py Outdated
Comment on lines +152 to +175
def _check_previous_probe(project_id: int) -> JobStatus | None:
# Missing key (first run, Redis eviction) means skip the check-in, not fail the tick.
try:
last_job_id = _redis_client.get(_LAST_JOB_ID_KEY)
except redis.RedisError as e:
logger.warning(f"[_check_previous_probe] Redis GET failed | error: {e}")
return None

if last_job_id is None:
logger.warning(
f"[_check_previous_probe] {_LAST_JOB_ID_KEY} missing — skipping check-in"
)
return None

with Session(engine) as session:
job = JobCrud(session=session).get(
job_id=UUID(str(last_job_id)), project_id=project_id
)

if job is None:
logger.warning(f"[_check_previous_probe] Job not found | job_id: {last_job_id}")
return None

return job.status

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unguarded UUID() parse can permanently stall the probe tick.

The Redis GET is protected against redis.RedisError, but UUID(str(last_job_id)) (and the subsequent JobCrud.get DB call) are not. If _LAST_JOB_ID_KEY ever holds a malformed value, ValueError propagates uncaught out of run_health_probe_tick, and since this happens before _store_last_job_id runs, the bad key is never overwritten — every future tick will hit the same value and fail identically, permanently breaking the health-probe cron until someone manually clears the Redis key. This is a stricter failure mode than the other Redis paths here, which all degrade gracefully.

🛡️ Proposed fix — degrade gracefully on malformed job id
-    with Session(engine) as session:
-        job = JobCrud(session=session).get(
-            job_id=UUID(str(last_job_id)), project_id=project_id
-        )
+    try:
+        job_uuid = UUID(str(last_job_id))
+    except ValueError as e:
+        logger.warning(
+            f"[_check_previous_probe] Malformed job id | value: {last_job_id!r}, error: {e}"
+        )
+        return None
+
+    with Session(engine) as session:
+        job = JobCrud(session=session).get(job_id=job_uuid, project_id=project_id)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _check_previous_probe(project_id: int) -> JobStatus | None:
# Missing key (first run, Redis eviction) means skip the check-in, not fail the tick.
try:
last_job_id = _redis_client.get(_LAST_JOB_ID_KEY)
except redis.RedisError as e:
logger.warning(f"[_check_previous_probe] Redis GET failed | error: {e}")
return None
if last_job_id is None:
logger.warning(
f"[_check_previous_probe] {_LAST_JOB_ID_KEY} missing — skipping check-in"
)
return None
with Session(engine) as session:
job = JobCrud(session=session).get(
job_id=UUID(str(last_job_id)), project_id=project_id
)
if job is None:
logger.warning(f"[_check_previous_probe] Job not found | job_id: {last_job_id}")
return None
return job.status
def _check_previous_probe(project_id: int) -> JobStatus | None:
# Missing key (first run, Redis eviction) means skip the check-in, not fail the tick.
try:
last_job_id = _redis_client.get(_LAST_JOB_ID_KEY)
except redis.RedisError as e:
logger.warning(f"[_check_previous_probe] Redis GET failed | error: {e}")
return None
if last_job_id is None:
logger.warning(
f"[_check_previous_probe] {_LAST_JOB_ID_KEY} missing — skipping check-in"
)
return None
try:
job_uuid = UUID(str(last_job_id))
except ValueError as e:
logger.warning(
f"[_check_previous_probe] Malformed job id | value: {last_job_id!r}, error: {e}"
)
return None
with Session(engine) as session:
job = JobCrud(session=session).get(job_id=job_uuid, project_id=project_id)
if job is None:
logger.warning(f"[_check_previous_probe] Job not found | job_id: {last_job_id}")
return None
return job.status
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/health_probes.py` around lines 152 - 175, Update
_check_previous_probe to handle malformed Redis job IDs without propagating an
exception: validate or parse last_job_id before calling JobCrud.get, catch UUID
parsing failures, log a warning consistent with the existing messages, and
return None so the health-probe tick can continue and later overwrite the key.
Also guard the JobCrud.get database call against its expected failure path,
logging the error and returning None while preserving the existing behavior for
missing jobs and valid statuses.

- The registry's ElevenLabs TTS and Sarvam TTS entries carry the provider-required Kaapi params (`voice` and `language` respectively) so those two probes pass, not merely fail loudly.
- Each cron tick tests one probe from the registry, round-robin, instead of all combinations at once.
- Sentry reflects the real outcome of a specific probe's job, not just that a Celery task was enqueued.
- A missing rotation-state key in Redis degrades to a logged, Sentry-visible entry and the flow continues; it never fails the cron tick.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing Redis-state handling is documented as Sentry-visible, but the implementation only logs or silently initializes it.

  • features/health-probes/SRD.md#L25-L25: add explicit skipped-state Sentry instrumentation, or remove the “Sentry-visible” requirement.
  • features/health-probes/SRD.md#L61-L61: either log missing health_probe:index explicitly or revise FR-4.
  • features/health-probes/assets/flow-a.mmd#L16-L18: show the logger-only branch unless a Sentry check-in is implemented.
📍 Affects 2 files
  • features/health-probes/SRD.md#L25-L25 (this comment)
  • features/health-probes/SRD.md#L61-L61
  • features/health-probes/assets/flow-a.mmd#L16-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/health-probes/SRD.md` at line 25, Align the documented missing
Redis-state behavior with the implementation: in features/health-probes/SRD.md
at lines 25 and 61, either add explicit Sentry instrumentation and logging for
missing rotation-state and health_probe:index values, or remove/revise those
Sentry-visible and explicit-logging requirements; in
features/health-probes/assets/flow-a.mmd lines 16-18, depict the logger-only
skipped-state branch unless Sentry check-in behavior is implemented.


- **Out of scope:** a probe results table or dashboard (the `job` table is the record of pass/fail); config storage for probes (each probe's config is hardcoded in its `LLMCallRequest` payload); alerting rules beyond the existing Sentry cron-monitor check-in pattern.
- **Cadence:** cron fires every `HEALTH_PROBE_INTERVAL_MINUTES` (~3); one probe per tick. With N probes in the registry, a given probe's effective retest cadence is `N * HEALTH_PROBE_INTERVAL_MINUTES`.
- **Rotation state, no new table:** the round-robin index and the last-fired job ID live in Redis (`REDIS_URL`, already Kaapi's Celery result backend), not Postgres. Keys: `health_probe:index` (advanced via atomic `INCR`, `mod` over registry length, so two overlapping ticks can never claim the same slot) and `health_probe:last_job_id`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not claim collision-free rotation during Redis errors.

_claim_next_probe_index() returns 0 for every RedisError, so overlapping ticks during a Redis outage can all select the same registry slot. Atomic INCR only provides the stated guarantee when it succeeds; update FR-9 and these design claims, or change the fallback behavior.

Also applies to: 66-66, 106-106

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/health-probes/SRD.md` at line 31, Update the Redis rotation claims
in SRD.md, including FR-9 and the references to health_probe:index, to state
that collision-free slot selection is guaranteed only when atomic Redis INCR
succeeds; document that RedisError fallback may cause overlapping ticks to
select the same probe.


---

**>> PLACE IMAGE HERE: `assets/flow-a.png`, cron tick, check previous probe result then fire the next.**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the image placeholder with the new diagram.

This still renders PLACE IMAGE HERE: assets/flow-a.png, while the change supplies assets/flow-a.mmd. Generate/link the image or embed the Mermaid source so the documented flow is visible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/health-probes/SRD.md` at line 45, Replace the PLACE IMAGE HERE
marker in SRD.md with the supplied assets/flow-a.mmd diagram, either by
embedding the Mermaid source or linking a generated image, so the cron/probe
flow renders visibly instead of showing placeholder text.

Comment on lines +72 to +74
### `GET /cron/health-probes` (existing, behavior replaced)

Still hidden from Swagger, still superuser-only. No longer enqueues a bespoke all-probes Celery task; instead runs the check-previous-then-fire-next tick described above and returns immediately once the next probe is enqueued.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Document the configured versus skipped cron response.

  • features/health-probes/SRD.md#L72-L74: document that missing org/project settings return enqueued: false, or change the route to fail visibly.
  • features/health-probes/assets/flow-a.mmd#L24-L24: add an alternate non-enqueued response branch.
📍 Affects 2 files
  • features/health-probes/SRD.md#L72-L74 (this comment)
  • features/health-probes/assets/flow-a.mmd#L24-L24
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/health-probes/SRD.md` around lines 72 - 74, Update
features/health-probes/SRD.md lines 72-74 to document that missing organization
or project settings cause the health-probes route to return enqueued: false, or
modify the route to fail visibly instead. Update
features/health-probes/assets/flow-a.mmd line 24 to show an alternate branch for
the non-enqueued response.

}
```

`previous_job_status` is `null` when `health_probe:last_job_id` was missing (first tick, or the key expired).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Describe every null-status path in the response contract.

previous_job_status is also null when Redis access fails, the referenced job is missing, or probe configuration is absent—not only when last_job_id is missing. Document these cases or expose a distinct skip/error reason.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/health-probes/SRD.md` at line 87, Update the response contract
around previous_job_status in SRD.md to document every path that yields null,
including Redis access failure, a missing referenced job, absent probe
configuration, and missing or expired health_probe:last_job_id. Alternatively,
expose distinct skip or error reasons for these cases instead of representing
them all as null.

Prajna1999 and others added 2 commits July 30, 2026 21:58
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ready-for-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Monitoring: Health checks for endpoints

4 participants